move user credentials to a primary route, update twitter agents and background job

Andrew Cantino vor 10 Jahren
Ursprung
Commit
ff758cc679

+ 0 - 1
app/assets/javascripts/application.js.coffee.erb

@@ -6,7 +6,6 @@
6 6
 #= require jquery.json-editor
7 7
 #= require latlon_and_geo
8 8
 #= require ./worker-checker
9
-#= require ./users
10 9
 #= require_self
11 10
 
12 11
 window.setupJsonEditor = ($editor = $(".live-json-editor")) ->

+ 0 - 13
app/assets/javascripts/users.js

@@ -1,13 +0,0 @@
1
-//alert("i get included");
2
-function remove_fields(link){
3
-  $(link).prev().val("1");
4
-  $(link).parent(".fields").hide();
5
-}
6
-
7
-function add_fields(link, association, content) {
8
-  var new_id = new Date().getTime();
9
-  var regexp = new RegExp("new_" + association, "g")
10
-  $(link).parent().before(content.replace(regexp, new_id));
11
-}
12
-
13
-

+ 27 - 14
app/concerns/twitter_concern.rb

@@ -2,28 +2,41 @@ module TwitterConcern
2 2
   extend ActiveSupport::Concern
3 3
 
4 4
   included do
5
-    self.validate :validate_twitter_options
6
-    self.after_initialize :configure_twitter
5
+    validate :validate_twitter_options
6
+    after_initialize :configure_twitter
7 7
   end
8 8
 
9 9
   def validate_twitter_options
10
-    unless options['consumer_key'].present? &&
11
-      options['consumer_secret'].present? &&
12
-      options['oauth_token'].present? &&
13
-      options['oauth_token_secret'].present?
14
-      errors.add(:base, "consumer_key, consumer_secret, oauth_token and oauth_token_secret are required to authenticate with the Twitter API")
10
+    unless twitter_consumer_key.present? &&
11
+      twitter_consumer_secret.present? &&
12
+      twitter_oauth_token.present? &&
13
+      twitter_oauth_token_secret.present?
14
+      errors.add(:base, "Twitter consumer_key, consumer_secret, oauth_token, and oauth_token_secret are required to authenticate with the Twitter API.  You can provide these as options to this Agent, or as Credentials with the same names, but starting with 'twitter_'.")
15 15
     end
16 16
   end
17 17
 
18
+  def twitter_consumer_key
19
+    options['consumer_key'].presence || credential('twitter_consumer_key')
20
+  end
21
+
22
+  def twitter_consumer_secret
23
+    options['consumer_secret'].presence || credential('twitter_consumer_secret')
24
+  end
25
+
26
+  def twitter_oauth_token
27
+    options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token')
28
+  end
29
+
30
+  def twitter_oauth_token_secret
31
+    options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret')
32
+  end
33
+
18 34
   def configure_twitter
19 35
     Twitter.configure do |config|
20
-      config.consumer_key = options['consumer_key']
21
-      config.consumer_secret = options['consumer_secret']
22
-      config.oauth_token = options['oauth_token'] || options['access_key']
23
-      config.oauth_token_secret = options['oauth_token_secret'] || options['access_secret']
36
+      config.consumer_key = twitter_consumer_key
37
+      config.consumer_secret = twitter_consumer_secret
38
+      config.oauth_token = twitter_oauth_token
39
+      config.oauth_token_secret = twitter_oauth_token_secret
24 40
     end
25 41
   end
26
-
27
-  module ClassMethods
28
-  end
29 42
 end

+ 61 - 0
app/controllers/user_credentials_controller.rb

@@ -0,0 +1,61 @@
1
+class UserCredentialsController < ApplicationController
2
+  def index
3
+    @user_credentials = current_user.user_credentials.page(params[:page])
4
+
5
+    respond_to do |format|
6
+      format.html
7
+      format.json { render json: @user_credentials }
8
+    end
9
+  end
10
+
11
+  def new
12
+    @user_credential = current_user.user_credentials.build
13
+
14
+    respond_to do |format|
15
+      format.html
16
+      format.json { render json: @user_credential }
17
+    end
18
+  end
19
+
20
+  def edit
21
+    @user_credential = current_user.user_credentials.find(params[:id])
22
+  end
23
+
24
+  def create
25
+    @user_credential = current_user.user_credentials.build(params[:user_credential])
26
+
27
+    respond_to do |format|
28
+      if @user_credential.save
29
+        format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully created.' }
30
+        format.json { render json: @user_credential, status: :created, location: @user_credential }
31
+      else
32
+        format.html { render action: "new" }
33
+        format.json { render json: @user_credential.errors, status: :unprocessable_entity }
34
+      end
35
+    end
36
+  end
37
+
38
+  def update
39
+    @user_credential = current_user.user_credentials.find(params[:id])
40
+
41
+    respond_to do |format|
42
+      if @user_credential.update_attributes(params[:user_credential])
43
+        format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully updated.' }
44
+        format.json { head :no_content }
45
+      else
46
+        format.html { render action: "edit" }
47
+        format.json { render json: @user_credential.errors, status: :unprocessable_entity }
48
+      end
49
+    end
50
+  end
51
+
52
+  def destroy
53
+    @user_credential = current_user.user_credentials.find(params[:id])
54
+    @user_credential.destroy
55
+
56
+    respond_to do |format|
57
+      format.html { redirect_to user_credentials_path }
58
+      format.json { head :no_content }
59
+    end
60
+  end
61
+end

+ 0 - 14
app/helpers/application_helper.rb

@@ -14,18 +14,4 @@ module ApplicationHelper
14 14
       link_to '<span class="label label-warning">No</span>'.html_safe, agent_path(agent, :tab => (agent.recent_error_logs? ? 'logs' : 'details'))
15 15
     end
16 16
   end
17
-
18
-  def link_to_remove_fields(name, f, options = {})
19
-    f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)", options)
20
-  end
21
-
22
-  def link_to_add_fields(name, f, options = {})
23
-    association = options[:association]   
24
-    new_object = f.object.class.reflect_on_association(association).klass.new
25
-    fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
26
-      render(association.to_s.singularize + "_fields", :f => builder)
27
-    end
28
-    link_to_function(name, "add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")")
29
-  end
30
-  
31 17
 end

+ 14 - 4
app/models/agent.rb

@@ -59,10 +59,6 @@ class Agent < ActiveRecord::Base
59 59
     where(:type => type)
60 60
   }
61 61
 
62
-  def credential(name)
63
-    user.user_credentials.where(:credential_name => name).first.try(:credential_value) || nil
64
-  end
65
-
66 62
   def check
67 63
     # Implement me in your subclass of Agent.
68 64
   end
@@ -106,6 +102,20 @@ class Agent < ActiveRecord::Base
106 102
     end
107 103
   end
108 104
 
105
+  def credential(name)
106
+    @credential_cache ||= {}
107
+    if @credential_cache.has_key?(name)
108
+      @credential_cache[name]
109
+    else
110
+      @credential_cache[name] = user.user_credentials.where(:credential_name => name).first.try(:credential_value)
111
+    end
112
+  end
113
+
114
+  def reload
115
+    @credential_cache = {}
116
+    super
117
+  end
118
+
109 119
   def new_event_expiration_date
110 120
     keep_events_for > 0 ? keep_events_for.days.from_now : nil
111 121
   end

+ 6 - 14
app/models/agents/twitter_publish_agent.rb

@@ -8,10 +8,11 @@ module Agents
8 8
     description <<-MD
9 9
       The TwitterPublishAgent publishes tweets from the events it receives.
10 10
 
11
-      You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`,
12
-      (also knows as "Access token" on the Twitter developer's site), along with the `username` of the Twitter user to publish as.
11
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
12
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
13
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
13 14
 
14
-      The `oauth_token` and `oauth_token_secret` determine which user the tweet will be sent as.
15
+      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
15 16
 
16 17
       You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet.
17 18
 
@@ -19,10 +20,7 @@ module Agents
19 20
     MD
20 21
 
21 22
     def validate_options
22
-      unless options['username'].present? &&
23
-        options['expected_update_period_in_days'].present?
24
-        errors.add(:base, "username and expected_update_period_in_days are required")
25
-      end      
23
+      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
26 24
     end
27 25
 
28 26
     def working?
@@ -31,12 +29,7 @@ module Agents
31 29
 
32 30
     def default_options
33 31
       {
34
-        'username' => "",
35 32
         'expected_update_period_in_days' => "10",
36
-        'consumer_key' => "---",
37
-        'consumer_secret' => "---",
38
-        'oauth_token' => "---",
39
-        'oauth_token_secret' => "---",
40 33
         'message_path' => "text"
41 34
       }
42 35
     end
@@ -68,9 +61,8 @@ module Agents
68 61
       end
69 62
     end
70 63
 
71
-    def publish_tweet text
64
+    def publish_tweet(text)
72 65
       Twitter.update(text)
73 66
     end
74
-
75 67
   end
76 68
 end

+ 8 - 10
app/models/agents/twitter_stream_agent.rb

@@ -6,11 +6,13 @@ module Agents
6 6
     description <<-MD
7 7
       The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide.
8 8
 
9
-      You must provide an oAuth `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`, as well as an array of `filters`.  Multiple words in a filter
10
-      must all show up in a tweet, but are independent of order.
11
-
9
+      To follow the Twitter stream, provide an array of `filters`.  Multiple words in a filter must all show up in a tweet, but are independent of order.
12 10
       If you provide an array instead of a filter, the first entry will be considered primary and any additional values will be treated as aliases.
13 11
 
12
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
+
14 16
       To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
15 17
 
16 18
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
@@ -67,13 +69,9 @@ module Agents
67 69
 
68 70
     def default_options
69 71
       {
70
-          'consumer_key' => "---",
71
-          'consumer_secret' => "---",
72
-          'oauth_token' => "---",
73
-          'oauth_token_secret' => "---",
74
-          'filters' => %w[keyword1 keyword2],
75
-          'expected_update_period_in_days' => "2",
76
-          'generate' => "events"
72
+        'filters' => %w[keyword1 keyword2],
73
+        'expected_update_period_in_days' => "2",
74
+        'generate' => "events"
77 75
       }
78 76
     end
79 77
 

+ 9 - 7
app/models/agents/twitter_user_agent.rb

@@ -9,7 +9,13 @@ module Agents
9 9
     description <<-MD
10 10
       The TwitterUserAgent follows the timeline of a specified Twitter user.
11 11
 
12
-      You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`, (Also shown as "Access token" on the Twitter developer's site.) along with the `username` of the Twitter user to monitor.
12
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
+
16
+      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
17
+
18
+      You must also provide the `username` of the Twitter user to monitor.
13 19
 
14 20
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
15 21
     MD
@@ -53,12 +59,8 @@ module Agents
53 59
 
54 60
     def default_options
55 61
       {
56
-          'username' => "tectonic",
57
-          'expected_update_period_in_days' => "2",
58
-          'consumer_key' => "---",
59
-          'consumer_secret' => "---",
60
-          'oauth_token' => "---",
61
-          'oauth_token_secret' => "---"
62
+        'username' => "tectonic",
63
+        'expected_update_period_in_days' => "2"
62 64
       }
63 65
     end
64 66
 

+ 1 - 0
app/models/contact.rb

@@ -1,5 +1,6 @@
1 1
 # Contacts are used only for the contact form on the Huginn website.  If you host a public Huginn instance, you can use
2 2
 # these to receive messages from visitors.
3
+
3 4
 class Contact < ActiveRecord::Base
4 5
   attr_accessible :email, :message, :name
5 6
 

+ 1 - 4
app/models/user.rb

@@ -22,10 +22,7 @@ class User < ActiveRecord::Base
22 22
   validates_format_of :username, :with => /\A[a-zA-Z0-9_-]{3,15}\Z/, :message => "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 15 characters in length."
23 23
   validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid"
24 24
 
25
-  has_many :user_credentials, :dependent => :destroy
26
-  accepts_nested_attributes_for :user_credentials,
27
-                                :allow_destroy => true
28
-  attr_accessible :user_credentials_attributes
25
+  has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
29 26
   has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user
30 27
   has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user
31 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"

+ 14 - 1
app/models/user_credential.rb

@@ -1,6 +1,19 @@
1 1
 class UserCredential < ActiveRecord::Base
2
-  attr_accessible :credential_name, :credential_value, :user_id
2
+  attr_accessible :credential_name, :credential_value
3
+
3 4
   belongs_to :user
5
+
4 6
   validates_presence_of :credential_name
7
+  validates_presence_of :credential_value
8
+  validates_presence_of :user_id
5 9
   validates_uniqueness_of :credential_name, :scope => :user_id
10
+
11
+  before_save :trim_fields
12
+
13
+  protected
14
+
15
+  def trim_fields
16
+    credential_name.strip!
17
+    credential_value.strip!
18
+  end
6 19
 end

+ 0 - 9
app/views/devise/registrations/_user_credential_fields.html.erb

@@ -1,9 +0,0 @@
1
-<p class="fields">
2
-  <%= f.label :credential_name, "Name" %>
3
-  <%= f.text_field :credential_name %>
4
-  <%= f.label :credential_value, "Value" %>
5
-  <%= f.text_field :credential_value %>
6
-  <%= f.hidden_field :_destroy %>
7
-  <%= link_to_remove_fields("remove", f) %>
8
-</p>
9
-

+ 0 - 6
app/views/devise/registrations/edit.html.erb

@@ -48,12 +48,6 @@
48 48
           <div class='form-actions'>
49 49
             <%= f.submit "Update", :class => "btn btn-primary" %>
50 50
           </div>
51
-          <div class="control-group">
52
-            <%= f.fields_for(:user_credentials) do |uc| %>
53
-              <%= render 'user_credential_fields', :f => uc %>
54
-            <% end %>
55
-          </div>
56
-          <p><%= link_to_add_fields "Add A Credential", f, :association => :user_credentials %></p>
57 51
         <% end %>
58 52
 
59 53
         <h3>Cancel my account</h3>

+ 1 - 0
app/views/layouts/_navigation.html.erb

@@ -4,6 +4,7 @@
4 4
   <ul class='nav pull-left'>
5 5
     <%= nav_link "Agents", agents_path %>
6 6
     <%= nav_link "Events", events_path %>
7
+    <%= nav_link "Credentials", user_credentials_path %>
7 8
   </ul>
8 9
 <% end %>
9 10
 

+ 30 - 0
app/views/user_credentials/_form.html.erb

@@ -0,0 +1,30 @@
1
+<%= form_for(@user_credential, :method => @user_credential.new_record? ? "POST" : "PUT") do |f| %>
2
+  <% if @user_credential.errors.any? %>
3
+    <div id="error_explanation">
4
+      <h2><%= pluralize(@user_credential.errors.count, "error") %> prohibited this Credential from being saved:</h2>
5
+      <ul>
6
+      <% @user_credential.errors.full_messages.each do |msg| %>
7
+        <li><%= msg %></li>
8
+      <% end %>
9
+      </ul>
10
+    </div>
11
+  <% end %>
12
+
13
+  <div class="control-group">
14
+    <%= f.label :credential_name, :class => 'control-label' %>
15
+    <div class="controls">
16
+      <%= f.text_field :credential_name, :class => 'span4' %>
17
+    </div>
18
+  </div>
19
+
20
+  <div class="control-group">
21
+    <%= f.label :credential_value, :class => 'control-label' %>
22
+    <div class="controls">
23
+      <%= f.text_area :credential_value, :class => 'span8', :rows => 10 %>
24
+    </div>
25
+  </div>
26
+
27
+  <div class='form-actions' style='clear: both'>
28
+    <%= f.submit "Save Credential", :class => "btn btn-primary" %>
29
+  </div>
30
+<% end %>

+ 17 - 0
app/views/user_credentials/edit.html.erb

@@ -0,0 +1,17 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Editing your Credential
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <div class="btn-group">
13
+        <%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, user_credentials_path, class: "btn" %>
14
+      </div>
15
+    </div>
16
+  </div>
17
+</div>

+ 44 - 0
app/views/user_credentials/index.html.erb

@@ -0,0 +1,44 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Your Credentials
7
+        </h2>
8
+      </div>
9
+
10
+      <blockquote>
11
+        Credentials are used to store values used by many Agents. Examples might include "twitter_consumer_secret",
12
+        "user_full_name", or "user_birthday".
13
+      </blockquote>
14
+
15
+      <table class='table table-striped'>
16
+        <tr>
17
+          <th>Name</th>
18
+          <th>Value</th>
19
+        </tr>
20
+
21
+        <% @user_credentials.each do |user_credential| %>
22
+          <tr>
23
+            <td><%= user_credential.credential_name %></td>
24
+            <td>
25
+              <%= truncate user_credential.credential_value %>
26
+              <div class="btn-group" style="float: right">
27
+                <%= link_to 'Edit', edit_user_credential_path(user_credential), class: "btn btn-mini" %>
28
+                <%= link_to 'Delete', user_credential_path(user_credential), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %>
29
+              </div>
30
+            </td>
31
+          </tr>
32
+        <% end %>
33
+      </table>
34
+
35
+      <%= paginate @user_credentials, :theme => 'twitter-bootstrap' %>
36
+
37
+      <br/>
38
+
39
+      <div class="btn-group">
40
+        <%= link_to '<i class="icon-plus"></i> New Credential'.html_safe, new_user_credential_path, class: "btn" %>
41
+      </div>
42
+    </div>
43
+  </div>
44
+</div>

+ 17 - 0
app/views/user_credentials/new.html.erb

@@ -0,0 +1,17 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Create a new Credential
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <div class="btn-group">
13
+        <%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, user_credentials_path, class: "btn" %>
14
+      </div>
15
+    </div>
16
+  </div>
17
+</div>

+ 9 - 11
bin/twitter_stream.rb

@@ -18,16 +18,16 @@ require 'twitter/json_stream'
18 18
 require 'em-http-request'
19 19
 require 'pp'
20 20
 
21
-def stream!(filters, options = {}, &block)
21
+def stream!(filters, agent, &block)
22 22
   stream = Twitter::JSONStream.connect(
23 23
     :path    => "/1/statuses/#{(filters && filters.length > 0) ? 'filter' : 'sample'}.json#{"?track=#{filters.map {|f| CGI::escape(f) }.join(",")}" if filters && filters.length > 0}",
24
+    :ssl     => true,
24 25
     :oauth   => {
25
-      :consumer_key    => options[:consumer_key],
26
-      :consumer_secret => options[:consumer_secret],
27
-      :access_key      => options[:oauth_token] || options[:access_key],
28
-      :access_secret   => options[:oauth_token_secret] || options[:access_secret]
29
-    },
30
-    :ssl     => true
26
+      :consumer_key    => agent.twitter_consumer_key,
27
+      :consumer_secret => agent.twitter_consumer_secret,
28
+      :access_key      => agent.twitter_oauth_token,
29
+      :access_secret   => agent.twitter_oauth_token_secret
30
+    }
31 31
   )
32 32
 
33 33
   stream.each_item do |status|
@@ -55,7 +55,7 @@ def stream!(filters, options = {}, &block)
55 55
 end
56 56
 
57 57
 def load_and_run(agents)
58
-  agents.group_by { |agent| agent.options[:twitter_username] }.each do |twitter_username, agents|
58
+  agents.group_by { |agent| agent.twitter_oauth_token }.each do |oauth_token, agents|
59 59
     filter_to_agent_map = agents.map { |agent| agent.options[:filters] }.flatten.uniq.compact.map(&:strip).inject({}) { |m, f| m[f] = []; m }
60 60
 
61 61
     agents.each do |agent|
@@ -64,11 +64,9 @@ def load_and_run(agents)
64 64
       end
65 65
     end
66 66
 
67
-    options = agents.first.options.slice(:consumer_key, :consumer_secret, :access_key, :oauth_token, :access_secret, :oauth_token_secret)
68
-
69 67
     recent_tweets = []
70 68
 
71
-    stream!(filter_to_agent_map.keys, options) do |status|
69
+    stream!(filter_to_agent_map.keys, agents.first) do |status|
72 70
       if status["retweeted_status"].present? && status["retweeted_status"].is_a?(Hash)
73 71
         puts "Skipping retweet: #{status["text"]}"
74 72
       elsif recent_tweets.include?(status["id_str"])

+ 2 - 0
config/routes.rb

@@ -26,6 +26,8 @@ Huginn::Application.routes.draw do
26 26
     end
27 27
   end
28 28
 
29
+  resources :user_credentials, :except => :show
30
+
29 31
   match "/worker_status" => "worker_status#show"
30 32
 
31 33
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"

+ 3 - 3
db/migrate/20140121075418_create_user_credentials.rb

@@ -1,9 +1,9 @@
1 1
 class CreateUserCredentials < ActiveRecord::Migration
2 2
   def change
3 3
     create_table :user_credentials do |t|
4
-      t.integer :user_id
5
-      t.string :credential_name
6
-      t.string :credential_value
4
+      t.integer :user_id,         :null => false
5
+      t.string :credential_name,  :null => false
6
+      t.text :credential_value,   :null => false
7 7
 
8 8
       t.timestamps
9 9
     end

+ 30 - 22
db/schema.rb

@@ -11,21 +11,21 @@
11 11
 #
12 12
 # It's strongly recommended to check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20140121075418) do
14
+ActiveRecord::Schema.define(:version => 20140127164931) do
15 15
 
16 16
   create_table "agent_logs", :force => true do |t|
17
-    t.integer  "agent_id",                         :null => false
18
-    t.text     "message",                          :null => false
19
-    t.integer  "level",             :default => 3, :null => false
17
+    t.integer  "agent_id",                                             :null => false
18
+    t.text     "message",           :limit => 16777215,                :null => false
19
+    t.integer  "level",                                 :default => 3, :null => false
20 20
     t.integer  "inbound_event_id"
21 21
     t.integer  "outbound_event_id"
22
-    t.datetime "created_at",                       :null => false
23
-    t.datetime "updated_at",                       :null => false
22
+    t.datetime "created_at",                                           :null => false
23
+    t.datetime "updated_at",                                           :null => false
24 24
   end
25 25
 
26 26
   create_table "agents", :force => true do |t|
27 27
     t.integer  "user_id"
28
-    t.text     "options"
28
+    t.text     "options",               :limit => 16777215
29 29
     t.string   "type"
30 30
     t.string   "name"
31 31
     t.string   "schedule"
@@ -37,27 +37,35 @@ ActiveRecord::Schema.define(:version => 20140121075418) do
37 37
     t.datetime "updated_at",                                                 :null => false
38 38
     t.text     "memory",                :limit => 2147483647
39 39
     t.datetime "last_webhook_at"
40
-    t.integer  "keep_events_for",                             :default => 0, :null => false
41 40
     t.datetime "last_event_at"
42 41
     t.datetime "last_error_log_at"
42
+    t.integer  "keep_events_for",                             :default => 0, :null => false
43 43
   end
44 44
 
45 45
   add_index "agents", ["schedule"], :name => "index_agents_on_schedule"
46 46
   add_index "agents", ["type"], :name => "index_agents_on_type"
47 47
   add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at"
48 48
 
49
+  create_table "contacts", :force => true do |t|
50
+    t.text     "message"
51
+    t.string   "name"
52
+    t.string   "email"
53
+    t.datetime "created_at", :null => false
54
+    t.datetime "updated_at", :null => false
55
+  end
56
+
49 57
   create_table "delayed_jobs", :force => true do |t|
50
-    t.integer  "priority",   :default => 0
51
-    t.integer  "attempts",   :default => 0
52
-    t.text     "handler"
53
-    t.text     "last_error"
58
+    t.integer  "priority",                       :default => 0
59
+    t.integer  "attempts",                       :default => 0
60
+    t.text     "handler",    :limit => 16777215
61
+    t.text     "last_error", :limit => 16777215
54 62
     t.datetime "run_at"
55 63
     t.datetime "locked_at"
56 64
     t.datetime "failed_at"
57 65
     t.string   "locked_by"
58 66
     t.string   "queue"
59
-    t.datetime "created_at",                :null => false
60
-    t.datetime "updated_at",                :null => false
67
+    t.datetime "created_at",                                    :null => false
68
+    t.datetime "updated_at",                                    :null => false
61 69
   end
62 70
 
63 71
   add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
@@ -65,11 +73,11 @@ ActiveRecord::Schema.define(:version => 20140121075418) do
65 73
   create_table "events", :force => true do |t|
66 74
     t.integer  "user_id"
67 75
     t.integer  "agent_id"
68
-    t.decimal  "lat",                            :precision => 15, :scale => 10
69
-    t.decimal  "lng",                            :precision => 15, :scale => 10
70
-    t.text     "payload",    :limit => 16777215
71
-    t.datetime "created_at",                                                     :null => false
72
-    t.datetime "updated_at",                                                     :null => false
76
+    t.decimal  "lat",                              :precision => 15, :scale => 10
77
+    t.decimal  "lng",                              :precision => 15, :scale => 10
78
+    t.text     "payload",    :limit => 2147483647
79
+    t.datetime "created_at",                                                       :null => false
80
+    t.datetime "updated_at",                                                       :null => false
73 81
     t.datetime "expires_at"
74 82
   end
75 83
 
@@ -88,9 +96,9 @@ ActiveRecord::Schema.define(:version => 20140121075418) do
88 96
   add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id"
89 97
 
90 98
   create_table "user_credentials", :force => true do |t|
91
-    t.integer  "user_id"
92
-    t.string   "credential_name"
93
-    t.string   "credential_value"
99
+    t.integer  "user_id",          :null => false
100
+    t.string   "credential_name",  :null => false
101
+    t.text     "credential_value", :null => false
94 102
     t.datetime "created_at",       :null => false
95 103
     t.datetime "updated_at",       :null => false
96 104
   end

+ 85 - 0
spec/controllers/user_credentials_controller_spec.rb

@@ -0,0 +1,85 @@
1
+require 'spec_helper'
2
+
3
+describe UserCredentialsController do
4
+  def valid_attributes(options = {})
5
+    {
6
+      :credential_name => "some_name",
7
+      :credential_value => "some_value"
8
+    }.merge(options)
9
+  end
10
+
11
+  before do
12
+    sign_in users(:bob)
13
+  end
14
+
15
+  describe "GET index" do
16
+    it "only returns UserCredentials for the current user" do
17
+      get :index
18
+      assigns(:user_credentials).all? {|i| i.user.should == users(:bob) }.should be_true
19
+    end
20
+  end
21
+
22
+  describe "GET edit" do
23
+    it "only shows UserCredentials for the current user" do
24
+      get :edit, :id => user_credentials(:bob_aws_secret).to_param
25
+      assigns(:user_credential).should eq(user_credentials(:bob_aws_secret))
26
+
27
+      lambda {
28
+        get :edit, :id => user_credentials(:jane_aws_secret).to_param
29
+      }.should raise_error(ActiveRecord::RecordNotFound)
30
+    end
31
+  end
32
+
33
+  describe "POST create" do
34
+    it "creates UserCredentials for the current user" do
35
+      expect {
36
+        post :create, :user_credential => valid_attributes
37
+      }.to change { users(:bob).user_credentials.count }.by(1)
38
+    end
39
+
40
+    it "shows errors" do
41
+      expect {
42
+        post :create, :user_credential => valid_attributes(:credential_name => "")
43
+      }.not_to change { users(:bob).user_credentials.count }
44
+      assigns(:user_credential).should have(1).errors_on(:credential_name)
45
+      response.should render_template("new")
46
+    end
47
+
48
+    it "will not create UserCredentials for other users" do
49
+      expect {
50
+        post :create, :user_credential => valid_attributes(:user_id => users(:jane).id)
51
+      }.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
52
+    end
53
+  end
54
+
55
+  describe "PUT update" do
56
+    it "updates attributes on UserCredentials for the current user" do
57
+      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
58
+      response.should redirect_to(user_credentials_path)
59
+      user_credentials(:bob_aws_key).reload.credential_name.should == "new_name"
60
+
61
+      lambda {
62
+        post :update, :id => user_credentials(:jane_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
63
+      }.should raise_error(ActiveRecord::RecordNotFound)
64
+      user_credentials(:jane_aws_key).reload.credential_name.should_not == "new_name"
65
+    end
66
+
67
+    it "shows errors" do
68
+      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "" }
69
+      assigns(:user_credential).should have(1).errors_on(:credential_name)
70
+      response.should render_template("edit")
71
+    end
72
+  end
73
+
74
+  describe "DELETE destroy" do
75
+    it "destroys only UserCredentials owned by the current user" do
76
+      expect {
77
+        delete :destroy, :id => user_credentials(:bob_aws_key).to_param
78
+      }.to change(UserCredential, :count).by(-1)
79
+
80
+      lambda {
81
+        delete :destroy, :id => user_credentials(:jane_aws_key).to_param
82
+      }.should raise_error(ActiveRecord::RecordNotFound)
83
+    end
84
+  end
85
+end

+ 10 - 2
spec/fixtures/user_credentials.yml

@@ -1,8 +1,16 @@
1 1
 bob_aws_key:
2 2
   user: bob
3 3
   credential_name: aws_key
4
-  credential_value: 2222222222
4
+  credential_value: 2222222222-bob
5 5
 bob_aws_secret:
6 6
   user: bob
7 7
   credential_name: aws_secret
8
-  credential_value: 1111111111
8
+  credential_value: 1111111111-bob
9
+jane_aws_key:
10
+  user: jane
11
+  credential_name: aws_key
12
+  credential_value: 2222222222-jane
13
+jane_aws_secret:
14
+  user: jane
15
+  credential_name: aws_secret
16
+  credential_value: 1111111111-jabe

+ 9 - 0
spec/models/agent_spec.rb

@@ -35,6 +35,15 @@ describe Agent do
35 35
     it "should return nil when credential is not present" do
36 36
       agents(:bob_weather_agent).credential("non_existing_credential").should == nil
37 37
     end
38
+
39
+    it "should memoize the load" do
40
+      mock.any_instance_of(UserCredential).credential_value.twice { "foo" }
41
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
42
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
43
+      agents(:bob_weather_agent).reload
44
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
45
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
46
+    end
38 47
   end
39 48
 
40 49
   describe "changes to type" do

+ 19 - 4
spec/models/user_credential_spec.rb

@@ -2,13 +2,28 @@ require 'spec_helper'
2 2
 
3 3
 describe UserCredential do
4 4
   describe "validation" do
5
-    it {should validate_uniqueness_of(:credential_name).scoped_to(:user_id)}
5
+    it { should validate_uniqueness_of(:credential_name).scoped_to(:user_id) }
6
+    it { should validate_presence_of(:credential_name) }
7
+    it { should validate_presence_of(:credential_value) }
8
+    it { should validate_presence_of(:user_id) }
6 9
   end
10
+
7 11
   describe "mass assignment" do
8
-    it {should allow_mass_assignment_of :credential_name}
12
+    it { should allow_mass_assignment_of :credential_name }
13
+
14
+    it { should allow_mass_assignment_of :credential_value }
9 15
 
10
-    it {should allow_mass_assignment_of :credential_value}
16
+    it { should_not allow_mass_assignment_of :user_id }
17
+  end
11 18
 
12
-    it {should allow_mass_assignment_of :user_id}
19
+  describe "cleaning fields" do
20
+    it "should trim whitespace" do
21
+      user_credential = user_credentials(:bob_aws_key)
22
+      user_credential.credential_name = " new name "
23
+      user_credential.credential_value = " new value "
24
+      user_credential.save!
25
+      user_credential.credential_name.should == "new name"
26
+      user_credential.credential_value.should == "new value"
27
+    end
13 28
   end
14 29
 end

+ 6 - 14
spec/models/users_spec.rb

@@ -4,24 +4,16 @@ describe User do
4 4
   describe "validations" do
5 5
     describe "invitation_code" do
6 6
       it "only accepts valid invitation codes" do
7
-      	User::INVITATION_CODES.each do |v|
8
-    			should allow_value(v).for(:invitation_code)
9
-  			end
7
+        User::INVITATION_CODES.each do |v|
8
+          should allow_value(v).for(:invitation_code)
9
+        end
10 10
       end
11 11
 
12 12
       it "can reject invalid invitation codes" do
13
-      	%w['foo', 'bar'].each do |v|
14
-    			should_not allow_value(v).for(:invitation_code)
15
-  			end
13
+        %w['foo', 'bar'].each do |v|
14
+          should_not allow_value(v).for(:invitation_code)
15
+        end
16 16
       end
17 17
     end
18 18
   end
19
-
20
-  describe "nested attributes" do
21
-    it { should accept_nested_attributes_for(:user_credentials).allow_destroy(true) }
22
-  end
23
-
24
-  describe "mass assignment" do
25
-    it {should allow_mass_assignment_of :user_credentials_attributes}
26
-  end
27 19
 end